<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Media Recorder and Playback Tool | Record and Test Your Audio and Video</title>
    <meta name="description" content="Use our Media Recorder and Playback Tool to easily record video and audio snippets without ads, sign-ins, or payments. Perfect for testing audio loudness and synchronization.">
    <meta name="keywords" content="media recorder, video recording, audio recording, test audio, test video, audio loudness, audio synchronization, no ads, no sign-in, no payment">
    <meta name="author" content="VDO.Ninja">
    <link rel="canonical" href="https://vdo.ninja/recorder">
	
	<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
	<link id="favicon1" rel="icon" type="image/png" sizes="32x32" href="../media/favicon-32x32.png" />
	<link id="favicon2" rel="icon" type="image/png" sizes="16x16" href="../media/favicon-16x16.png" />
	<link id="favicon3" rel="icon" href="../media/favicon.ico" />
	<link id="thumbnailUrl" itemprop="thumbnailUrl" href="../media/vdoNinja_logo_full.png" />
		
    <style>
        body {
			display: flex;
			justify-content: center;
			align-items: center;
			min-height: 100vh;
			margin: 0;
			background-color: #2b2b2b;
			font-family: 'Helvetica Neue', Arial, sans-serif;
			color: #e0e0e0;
		}
		#container {
			text-align: center;
			width: 90%;
			max-width: 800px;
			padding: 20px;
			background: #3a3a3a;
			border-radius: 10px;
			box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
		}
		h1 {
			font-size: 24px;
			margin-bottom: 15px;
			color: #ffffff;
		}
		#video {
			display: block;
			margin: 10px auto;
			max-width: 100%;
			max-height: 50vh;
			border-radius: 10px;
			box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
		}
		#audio-meter {
			width: 100%;
			height: 20px;
			background: #444;
			margin-top: 10px;
			border-radius: 10px;
			overflow: hidden;
			box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
		}
		#meter-fill {
			width: 0%;
			height: 100%;
			background: linear-gradient(90deg, #4caf50, #76ff0322);
			border-radius: 10px;
		}
		#controls {
			margin-top: 15px;
			display: flex;
			flex-wrap: wrap;
			justify-content: center;
		}
		select, button {
			margin: 5px;
			padding: 8px 15px;
			font-size: 14px;
			border: none;
			border-radius: 5px;
			cursor: pointer;
			background-color: #007bff;
			color: white;
			box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
			transition: background-color 0.2s;
		}
		button:hover {
			background-color: #0056b3;
		}
		select {
			background-color: #555;
			color: #e0e0e0;
			padding: 8px;
			border: 1px solid #666;
		}
		#monitor {
			background-color: #ff9800;
			color: #1a1a1a;
			font-weight: 600;
		}
		#monitor.muted {
			background-color: #666;
			color: #f0f0f0;
		}
		.recording-indicator {
			color: red;
			animation: pulse 1s infinite;
			margin-top: 10px;
		}
		@keyframes pulse {
			0% { opacity: 1; }
			50% { opacity: 0.5; }
			100% { opacity: 1; }
		}
		.hidden {
			display: none;
		}

		@media (max-width: 600px) {
			#container {
				padding: 10px;
			}
			h1 {
				font-size: 20px;
			}
			select, button {
				padding: 6px 12px;
				font-size: 12px;
			}
		}

		@media (max-height: 500px) {
			#container {
				height: auto;
				overflow-y: auto;
			}
			#video {
				max-height: 40vh;
			}
		}
		#video {
			background-color: #000;
		}
    </style>
</head>
<body>
    <div id="container">
        <h1>Media Recorder and Playback Tool</h1>
        <select id="videoSource"></select>
        <select id="audioSource"></select>
        <video id="video" autoplay muted></video>
        <div id="audio-meter">
            <div id="meter-fill"></div>
        </div>
        <div id="controls">
            <button id="monitor" class="muted">Unmute Monitor</button>
            <button id="record" disabled>Record</button>
            <button id="play" class="hidden">Play</button>
            <button id="download" class="hidden">Download</button>
            <div id="recording-indicator" class="hidden recording-indicator">Recording...</div>
        </div>
    </div>
    <script>
        const videoElement = document.getElementById('video');
        const recordButton = document.getElementById('record');
        const playButton = document.getElementById('play');
        const downloadButton = document.getElementById('download');
        const meterFill = document.getElementById('meter-fill');
        const videoSelect = document.getElementById('videoSource');
        const audioSelect = document.getElementById('audioSource');
        const recordingIndicator = document.getElementById('recording-indicator');
        const monitorButton = document.getElementById('monitor');

        let mediaStream = null;
        let mediaRecorder = null;
        let recordedChunks = [];
        let audioContext = null;
        let meter = null;
        let sourceNode = null;
        let recordedBlob = null;
        let monitorGain = null;
        let monitorEnabled = false;

		if (monitorButton) {
			monitorButton.setAttribute('disabled', 'true');
		}

		(function (w) {
				w.URLSearchParams =
					w.URLSearchParams ||
					function (searchString) {
						var self = this;
						self.searchString = searchString;
						self.get = function (name) {
							var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(self.searchString);
							if (results == null) {
								return null;
							} else {
								return decodeURI(results[1]) || 0;
							}
						};
					};
			})(window);
			
		var urlParams = new URLSearchParams(window.location.search);
		
		var denoise = false;
		if (urlParams.has("denoise")){
			denoise = true;
			console.log("De-noising enabled");
		}
		
		var autogain = true;
		if (urlParams.has("noautogain")){
			autogain = false;
			console.log("Auto-gain disabled");
		}

		async function init() {
			try {
				await getDevices();
				await startMedia().catch(e => {
					console.warn('Initial media start failed:', e);
					// Still allow device selection even if initial device is busy
					recordButton.setAttribute("disabled", "true");
					videoElement.srcObject = null;
					videoElement.poster = "camera-off.png"; // Optional: show offline state
				});
			} catch (error) {
				console.error('Error initializing:', error);
			}
		}

		async function getDevices() {
			try {
				// Request initial permissions - required for Firefox
				await navigator.mediaDevices.getUserMedia({ audio: true, video: true })
					.then(stream => stream.getTracks().forEach(track => track.stop()))
					.catch(e => console.warn('Permission denied:', e));
					
				const devices = await navigator.mediaDevices.enumerateDevices();
				const videoDevices = devices.filter(device => device.kind === 'videoinput');
				const audioDevices = devices.filter(device => device.kind === 'audioinput');

				// Only update selects if we have device labels (permissions granted)
				if (devices.some(device => device.label)) {
					videoSelect.innerHTML = videoDevices.map(device => 
						`<option value="${device.deviceId}">${device.label || `Video Device ${videoDevices.indexOf(device) + 1}`}</option>`
					).join('');
					audioSelect.innerHTML = audioDevices.map(device => 
						`<option value="${device.deviceId}">${device.label || `Audio Device ${audioDevices.indexOf(device) + 1}`}</option>`
					).join('');
				}
			} catch (error) {
				console.error('Error getting devices:', error);
			}
		}

        function updateMonitorButton() {
			if (!monitorButton) {
				return;
			}
			if (monitorEnabled) {
				monitorButton.textContent = 'Mute Monitor';
				monitorButton.classList.remove('muted');
			} else {
				monitorButton.textContent = 'Unmute Monitor';
				monitorButton.classList.add('muted');
			}
		}

		function applyMonitorState() {
			if (monitorGain && audioContext) {
				monitorGain.gain.setTargetAtTime(monitorEnabled ? 1 : 0, audioContext.currentTime, 0.01);
			}
		}

        async function startMedia() {
			try {
				if (mediaStream) {
					mediaStream.getTracks().forEach(track => track.stop());
				}

				const videoSource = videoSelect.value;
				const audioSource = audioSelect.value;

				// Try video and audio separately to handle partial failures
				try {
					mediaStream = await navigator.mediaDevices.getUserMedia({
						video: { deviceId: videoSource ? { exact: videoSource } : undefined },
						audio: {
							deviceId: audioSource ? { exact: audioSource } : undefined,
							echoCancellation: false,
							noiseSuppression: denoise,
							autoGainControl: autogain
						}
					});
				} catch (e) {
					// If full request fails, try audio-only
					console.warn('Full media request failed, trying audio-only:', e);
					mediaStream = await navigator.mediaDevices.getUserMedia({
						video: false,
						audio: {
							deviceId: audioSource ? { exact: audioSource } : undefined,
							echoCancellation: false,
							noiseSuppression: denoise,
							autoGainControl: autogain
						}
					});
				}
				if (mediaStream.getVideoTracks().length > 0) {
					videoElement.srcObject = mediaStream;
					recordButton.removeAttribute("disabled");
				} else {
					videoElement.srcObject = null;
					videoElement.poster = "camera-off.png"; // Optional: show offline state
					recordButton.setAttribute("disabled", "true");
				}

				if (audioContext) {
					try {
						await audioContext.close();
					} catch (closeError) {
						console.warn('Error closing previous audio context:', closeError);
					}
					audioContext = null;
					meter = null;
					monitorGain = null;
					sourceNode = null;
				}
				audioContext = new (window.AudioContext || window.webkitAudioContext)();
				monitorGain = audioContext.createGain();
				monitorGain.gain.value = monitorEnabled ? 1 : 0;
				monitorGain.connect(audioContext.destination);
				await audioContext.audioWorklet.addModule('testrecord-processor.js');

				const audioTracks = mediaStream.getAudioTracks();
				if (audioTracks.length > 0) {
					sourceNode = audioContext.createMediaStreamSource(mediaStream);
					meter = new AudioWorkletNode(audioContext, 'meter');
					meter.port.onmessage = (event) => {
						const volume = event.data;
						meterFill.style.width = `${Math.min(volume * 100, 100)}%`;
					};
					sourceNode.connect(meter);
					sourceNode.connect(monitorGain);
					applyMonitorState();
					if (monitorButton) {
						monitorButton.removeAttribute('disabled');
					}
				} else if (monitorButton) {
					monitorEnabled = false;
					applyMonitorState();
					monitorButton.setAttribute('disabled', 'true');
				}
				updateMonitorButton();

			} catch (error) {
				if (monitorButton) {
					monitorButton.setAttribute('disabled', 'true');
				}
				console.error('Error starting media:', error);
				throw error; // Re-throw to allow handling by caller
			}
		}

        recordButton.addEventListener('click', async () => {
            try {
                await audioContext.resume();
                if (mediaRecorder && mediaRecorder.state === 'recording') {
                    mediaRecorder.stop();
                    recordButton.textContent = 'Record';
                    recordingIndicator.classList.add('hidden');
                    videoSelect.removeAttribute("disabled");
                    audioSelect.removeAttribute("disabled");
                } else {
                    // Show live camera feed again
                    videoElement.srcObject = mediaStream;
                    videoElement.controls = false;
                    videoElement.muted = true;
                    playButton.classList.add('hidden');
                    downloadButton.classList.add('hidden');

                    recordedChunks = [];
                    mediaRecorder = new MediaRecorder(mediaStream);
                    mediaRecorder.ondataavailable = event => {
                        if (event.data.size > 0) {
                            recordedChunks.push(event.data);
                        }
                    };
                    mediaRecorder.onstop = () => {
                        recordedBlob = new Blob(recordedChunks, { type: 'video/webm' });
                        videoElement.srcObject = null;
                        videoElement.src = URL.createObjectURL(recordedBlob);
                        videoElement.controls = true;
                        videoElement.loop = true;
                        videoElement.muted = false; // Unmute during playback
                        playButton.classList.remove('hidden');
                        downloadButton.classList.remove('hidden');
                        playButton.textContent = 'Play';
                        videoElement.pause(); // Ensure the video does not autoplay
                    };
                    mediaRecorder.start();
                    recordButton.textContent = 'Stop';
                    recordingIndicator.classList.remove('hidden');
                    videoSelect.setAttribute("disabled", "true");
                    audioSelect.setAttribute("disabled", "true");
                }
            } catch (error) {
                console.error('Error during recording:', error);
            }
        });

        function updatePlayButtonText() {
			playButton.textContent = videoElement.paused ? 'Play' : 'Stop';
		}

		videoElement.addEventListener('play', updatePlayButtonText);
		videoElement.addEventListener('pause', updatePlayButtonText);

		playButton.addEventListener('click', () => {
			if (videoElement.paused) {
				videoElement.play();
			} else {
				videoElement.pause();
			}
			updatePlayButtonText();
		});

        downloadButton.addEventListener('click', () => {
            const url = URL.createObjectURL(recordedBlob);
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            a.download = 'recording.webm';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });

		if (monitorButton) {
			monitorButton.addEventListener('click', async () => {
				if (!audioContext) {
					return;
				}
				try {
					await audioContext.resume();
				} catch (error) {
					console.warn('Unable to resume audio context:', error);
				}
				monitorEnabled = !monitorEnabled;
				applyMonitorState();
				updateMonitorButton();
			});
		}

        videoSelect.onchange = startMedia;
        audioSelect.onchange = startMedia;

        init();
    </script>
</body>
</html>
